using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Threading;
using Lync.Widget.Library.Configuration;
using Lync.Widget.Library.UserService;
using Microsoft.Rtc.Collaboration.Presence;
using log4net;

namespace Lync.Widget.Library.Presence
{
    /// <summary>
    /// The default UCMA implementation of IPresenceCache.
    /// Gathers and caches presence updates for the requested sip uri(s)
    /// </summary>
    public class UcPresenceCache : IPresenceCache
    {
        private static readonly ILog Log = LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType);
        private Dictionary<string, PresenceSubscription> m_presenceSubscriptionsBySipUri =
            new Dictionary<string, PresenceSubscription>(StringComparer.CurrentCultureIgnoreCase);
        private object m_syncObject = new object();
        private Timer _staleTimer;
        private DateTime StaleTime { get { return DateTime.Now - StaleInterval; } }
        private TimeSpan ResubscriptionTime = TimeSpan.FromMinutes(5);
        private const int MaxSubscriptionResponseTimeMinutes = 5;
        private Timer _resubscriptionTimer;

        /// <summary>
        /// When set to true, unsubscribe and resubscribe after every time we received presence information.
        /// Implemented because the presence information would only be given once after subscription in our environment.
        /// </summary>
        private bool circumventSubscription = true;

        private TimeSpan resubscribeDelay = TimeSpan.FromSeconds(4);

        private IPresenceProvider m_presenceProvider;

        private Dictionary<string, Timer> m_presenceTimersBySipUri = new Dictionary<string, Timer>(StringComparer.CurrentCultureIgnoreCase);

        /// <summary>
        /// The interval to run cache cleanup. If any presence subscriptions have not been requested in the last stale interval
        /// they will be removed from the cache
        /// </summary>
        public TimeSpan StaleInterval { get; set; }

        public UcPresenceCache(IPresenceProvider provider)
        {
            m_presenceProvider = provider;
            m_presenceProvider.PresenceChanged += PresenceProvider_PresenceChanged;
            StaleInterval = TimeSpan.FromHours(2);
            _staleTimer = new Timer(OnStaleTimerTick, null, StaleInterval, TimeSpan.FromMilliseconds(-1));
            _resubscriptionTimer = new Timer(OnResubscriptionTimerTick, null, ResubscriptionTime, TimeSpan.FromMilliseconds(-1));
        }

        public PresenceSubscription GetPresence(string sipUri, int priority = -1)
        {
            lock (m_syncObject)
            {
                if (!m_presenceSubscriptionsBySipUri.ContainsKey(sipUri))
                {
                    m_presenceSubscriptionsBySipUri.Add(sipUri, new PresenceSubscription
                    {
                        Availability = (long)PresenceAvailability.Away,
                        ContactName = Consts.Unknown,
                        SipUri = sipUri,
                        Status = Consts.Updating,
                        Priority = priority,
                        SubscriptionCreated = DateTime.Now
                    });

                    Log.DebugFormat("Not in a cache - start subscription for sip uri '{0}'", sipUri);

                    m_presenceProvider.SubscribeToPresence(sipUri);
                }
                else
                {
                    Log.DebugFormat("Presence Subscriptions Dictionary already contains sip uri '{0}'", sipUri);
                }

                //update the timestamp whenever the presence is requested
                //this prevents a presence from being cleared out of the cache at the StaleInterval
                m_presenceSubscriptionsBySipUri[sipUri].SubscriptionCreated = DateTime.Now;
                return m_presenceSubscriptionsBySipUri[sipUri];
            }
        }

        public PresenceSubscription GetCachedPresence(string sipUri)
        {
            PresenceSubscription presence;

            lock (m_syncObject)
            {
                m_presenceSubscriptionsBySipUri.TryGetValue(sipUri, out presence);
            }

            if (presence != null && presence.Status == Consts.Updating)
            {
                presence = null;
            }

            return presence;
        }

        public PresenceSubscription GetInitialPresence(string sipUri)
        {
            return new PresenceSubscription
            {
                Availability = (long)PresenceAvailability.Away,
                ContactName = Consts.Unknown,
                SipUri = sipUri,
                Priority = -1,
                SubscriptionCreated = DateTime.Now,
                Status = Consts.Updating,
                Email = sipUri.Substring(4, sipUri.Length - 4)
            };    
        }

        public void FilterByActiveDirectoryUser(PresenceSubscription presenceSubscription, User adUser)
        {
            
        }

        public IList<GroupSubscription> GetGroupPresence(IList<IActiveDirectoryGroup> groups)
        {
            return groups != null
                ? groups.Select(Projection).ToList()
                : new List<GroupSubscription>();
        }

        public GroupSubscription Projection(IActiveDirectoryGroup group)
        {
            var presenceSubscriptions = group.Members.Select(m => GetPresence(m.Sip, m.Priority)).ToList();

            var availability = GetPresenceAvailabilityForGroupMembers(presenceSubscriptions);

            var status = AvailabilityToStatusConverter.Convert((long)availability);

            var subscription = new GroupSubscription
            {
                GroupId = group.Id,
                GroupName = group.Name,
                HuntGroupNumber = group.HuntgroupQuantity,
                TimeToWaitInSeconds = group.TimeToWaitInSeconds,
                Presences = presenceSubscriptions,
                CanChat = availability != PresenceAvailability.Offline,
                Availability = (long)availability,
                Status = status,
            };

            return subscription;
        }

        public List<PresenceSubscription> GetPresence(List<string> sipUris)
        {
            List<PresenceSubscription> presences = new List<PresenceSubscription>();
            foreach (string sipUri in sipUris)
            {
                presences.Add(GetPresence(sipUri));
            }
            return presences;
        }

        #region Manage Presence Changes
        private void PresenceProvider_PresenceChanged(object sender, PresenceChangedEventArgs e)
        {
            lock (m_syncObject)
            {
                foreach (PresenceSubscription presence in e.Presences)
                {
                    if (m_presenceSubscriptionsBySipUri.ContainsKey(presence.SipUri))
                    {
                        UpdatePresenceSubscriptionWithChanges(presence);

                        if (circumventSubscription)
                        {
                            ResubscribeToPresence(presence.SipUri);
                        }
                    }
                    else //received a presence changed event for an expired subscription
                    {
                        Log.InfoFormat("Ignoring presence update for sipUri '{0}' since there are no active subscriptions for this uri", presence.SipUri);
                        m_presenceProvider.UnsubscribeToPresence(presence.SipUri);
                        if (m_presenceTimersBySipUri.ContainsKey(presence.SipUri))
                        {
                            m_presenceTimersBySipUri.Remove(presence.SipUri);
                        }
                    }
                }
            }
        }

        private void ResubscribeToPresence(string sipUri)
        {
            m_presenceProvider.UnsubscribeToPresence(sipUri);
            if (m_presenceTimersBySipUri.ContainsKey(sipUri))
            {
                m_presenceTimersBySipUri.Remove(sipUri);
            }

            Timer timer = new Timer(o => m_presenceProvider.SubscribeToPresence(sipUri), null, resubscribeDelay, TimeSpan.FromMilliseconds(-1));
            m_presenceTimersBySipUri.Add(sipUri, timer);
        }

        private void UpdatePresenceSubscriptionWithChanges(PresenceSubscription presence)
        {
            if (presence.Availability != PresenceSubscription.NoChangeInAvailability)
            {
                m_presenceSubscriptionsBySipUri[presence.SipUri].Availability = presence.Availability;
            }
            if (presence.ContactName != PresenceSubscription.NoChange)
            {
                m_presenceSubscriptionsBySipUri[presence.SipUri].ContactName = presence.ContactName;
            }
            if (presence.Status != PresenceSubscription.NoChange)
            {
                m_presenceSubscriptionsBySipUri[presence.SipUri].Status = presence.Status;
            }
            if (presence.Email != PresenceSubscription.NoChange)
            {
                m_presenceSubscriptionsBySipUri[presence.SipUri].Email = presence.Email;
            }
            if (presence.Phone != PresenceSubscription.NoChange)
            {
                m_presenceSubscriptionsBySipUri[presence.SipUri].Phone = presence.Phone;
            }
            if (presence.Mobile != PresenceSubscription.NoChange)
            {
                m_presenceSubscriptionsBySipUri[presence.SipUri].Mobile = presence.Mobile;
            }
            if (presence.JobTitle != PresenceSubscription.NoChange)
            {
                m_presenceSubscriptionsBySipUri[presence.SipUri].JobTitle = presence.JobTitle;
            }

            m_presenceSubscriptionsBySipUri[presence.SipUri].NotificationReceivedTime = DateTime.Now;

            m_presenceSubscriptionsBySipUri[presence.SipUri].CanChat = CanPresenceChat(presence);
        }

        
        #endregion

        #region Cache Cleanup
        private void RemoveStaleSubscriptions()
        {
            lock (m_syncObject)
            {
                var staleKvps = m_presenceSubscriptionsBySipUri.Where(kvp => kvp.Value.SubscriptionCreated <= StaleTime).ToList();
                foreach (var staleKvp in staleKvps)
                {
                    string sipUri = staleKvp.Key;
                    Log.Info(string.Format("Removing stale subscription for sip uri '{0}', last requested at {1}", sipUri, staleKvp.Value.SubscriptionCreated));
                    m_presenceProvider.UnsubscribeToPresence(sipUri);
                    bool isRemoved = m_presenceSubscriptionsBySipUri.Remove(sipUri);
                    Log.Info(sipUri + (isRemoved ? " IS" : " IS NOT") + " removed from the Presence Subscriptions Dictionary!");
                    if (!isRemoved)
                    {
                        Log.InfoFormat(
                            "Current list of subscribers in the Presence Subscriptions Dictionary: '{0}'",
                            string.Join(",", m_presenceSubscriptionsBySipUri.Keys));
                    }

                    isRemoved = m_presenceTimersBySipUri.Remove(sipUri);
                    Log.Info(sipUri + (isRemoved ? " IS" : " IS NOT") + " removed from the Presence Timers Dictionary!");
                }
            }
        }

        private void OnStaleTimerTick(object state)
        {
            //stop the stale timer
            _staleTimer.Change(TimeSpan.FromMilliseconds(-1), TimeSpan.FromMilliseconds(-1));
            RemoveStaleSubscriptions();
            _staleTimer.Change(StaleInterval, TimeSpan.FromMilliseconds(-1));
        }

        #endregion
        
        private void ResubscribeNotRespondingSubscriptions()
        {
            lock (m_syncObject)
            {
                IEnumerable<KeyValuePair<string, PresenceSubscription>> notActiveSubscriptions =
                    m_presenceSubscriptionsBySipUri.Where(
                        kvp =>
                        kvp.Value.NotificationReceivedTime > kvp.Value.SubscriptionCreated.AddMinutes(MaxSubscriptionResponseTimeMinutes));

                foreach (var staleKvp in notActiveSubscriptions)
                {
                    string sipUri = staleKvp.Key;

                    Log.InfoFormat(
                        "Resubscribing to the presence of sip uri '{0}', last requested at {1}. Last Lync notification: {2}",
                        sipUri, staleKvp.Value.SubscriptionCreated, staleKvp.Value.NotificationReceivedTime);

                    ResubscribeToPresence(sipUri);
                }
            }
        }
        
        private void OnResubscriptionTimerTick(object state)
        {
            //stop the timer
            _resubscriptionTimer.Change(TimeSpan.FromMilliseconds(-1), TimeSpan.FromMilliseconds(-1));
            ResubscribeNotRespondingSubscriptions();
            _resubscriptionTimer.Change(ResubscriptionTime, TimeSpan.FromMilliseconds(-1));
        }

        private bool CanPresenceChat(PresenceSubscription presence)
        {
            return (presence.Availability >= 3000 && presence.Availability <= 4499)
                   || (presence.Availability >= 6000 && presence.Availability <= 7499);
        }

        public void Reset(string sipUri)
        {
            m_presenceProvider.UnsubscribeToPresence(sipUri);
            m_presenceSubscriptionsBySipUri.Remove(sipUri);
        }

        public void ResetAll()
        {
            foreach (var sipUri in m_presenceSubscriptionsBySipUri.Keys)
            {
                m_presenceProvider.UnsubscribeToPresence(sipUri);
            }

            m_presenceSubscriptionsBySipUri = new Dictionary<string, PresenceSubscription>();
        }

        #region Private Methods Groups

        private PresenceAvailability GetPresenceAvailabilityForGroupMembers(IList<PresenceSubscription> subscriptions)
        {
            PresenceAvailability availability;

            var listOfAvailibility =
                subscriptions.Select(presence => AvailabilityRangeConverter.Convert(presence.Availability)).ToList();

            if (listOfAvailibility.Count(a => a == PresenceAvailability.Online) >= 1)
            {
                availability = PresenceAvailability.Online;
            }
            else if (listOfAvailibility.Count(a => a == PresenceAvailability.Busy) >= 1)
            {
                availability = PresenceAvailability.Busy;
            }
            else
            {
                availability = PresenceAvailability.Offline;
            }
            return availability;
        }

        #endregion

    }
}

